今天又是在 deadline 前完成任務的一天。在處理這種實作比較多的題目時,確實需要投入更多時間和精力。然而,我參加這次鐵人賽的初衷是將其設定為一個非常自由的挑戰,原本的想法就是「想到什麼寫什麼」,因此每天都是靠靈感即興發揮。說真的,我並沒有特別去做事前準備,每次都是下班吃完晚餐後,在洗澡時稍微思考一下今天該寫的主題,然後靈光一閃,就開始撰寫。基本上我的寫作流程就是這麼簡單、隨性,也因此常常落得在 deadline 前趕工想內容的窘境。
在昨日的文章中我曾提及,嘗試使用 Spring AI 進行 Embedding,當時使用了 OpenAI 的 text-embedding-3-small
模型,將文字轉換成向量,並選擇 Qdrant 作為向量資料庫來儲存這些轉換後的向量。然而,測試過程中遇到了一些問題,一度無法釐清問題所在,但今日終於弄清楚了整個前因後果。
其實問題很單純,就是我在使用 Qdrant 的時候,是透過 Qdrant Console 手動進行一些操作,像是建立 Collection,而不是透過 Spring AI 自動初始化。在 Qdrant 裡手動建立 Collection 的時候,如果只是給它命名,卻沒有設定相關的參數,那麼在寫入向量的時候就會出現問題。
這就是整個問題的癥結點!
text-embedding-3-small
模型為例,它的向量大小是 1536,這個數字必須在 Collection 初始化時指定。如果沒有在初始化 Collection 時設定好這些參數,那麼向量就無法正確存入 Qdrant。我之前就是忽略了這些設定,結果搞得自己一頭霧水。還好問題並不複雜,今天順利解決了!
在解決 Qdrant 的問題後,我決定更進一步,著手建立一個簡易的文字檔案上傳 API。由於 Spring AI 本身就提供了 ETL Pipeline 的功能,因此整個流程相當順暢。以下是我的實作步驟:
檔案讀取:利用 Spring AI 的 TextReader
來讀取上傳的 TXT 檔案,並將內容轉換成 Document 物件的列表。
文字分段:接著,透過 TokenTextSplitter
將文件切割成較小的片段(Chunks),其預設使用 CL100K_BASE
編碼,確保文字能被有效處理。
向量化與儲存:最後,使用 VectorStore
的 Add
方法,將這些分段後的文字片段進行 Embedding,轉換成向量後儲存到 Vector Store 中。
整個流程相當直觀,難度不高,但對於建立基本的文字處理流程來說,非常實用。
@PostMapping("/api/upload")
fun uploadAndProcessDocument(
@RequestParam(
value = "file"
) file: MultipartFile
): ResponseEntity<Map<*, *>> {
if (file.isEmpty) {
return ResponseEntity.badRequest().body(java.util.Map.of("error","Please upload a non-empty file."))
}
if (!file.originalFilename?.endsWith(".txt", ignoreCase = true)!!) {
return ResponseEntity.badRequest().body(java.util.Map.of("error","Please upload a .txt file."))
}
val textReader = TextReader(file.resource)
val documents = textReader.get()
val tokenTextSplitter = TokenTextSplitter()
val splitDocuments = tokenTextSplitter.apply(documents)
vectorStore.add(splitDocuments)
return ResponseEntity.ok(java.util.Map.of("upload", "ok"))
}
今天的工作進度大致分享到這裡,主要完成了昨天 Qdrant 向量儲存遇到的問題,並成功建置一個簡易的文字上傳 API。上傳的文字內容會經過 ETL Pipeline 處理後,儲存至 Vector Store。由於明天是颱風假,我會思考看看還有什麼新東西可以嘗試,或許會有更多有趣的靈感出現!
也祝各位好好享受颱風假,希望明天我們都有更多創作的機會!